# -*- coding: utf-8 -*-


"""

版权所有 © 2025 金羿ELS
Copyright (c) 2025 Eilles(EillesWan@outlook.com)

MineMusicVisualizer is licensed under Mulan PSL v2.
You can use this software according to the terms and conditions of the Mulan PSL v2.
You may obtain a copy of Mulan PSL v2 at:
         http://license.coscl.org.cn/MulanPSL2
THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
See the Mulan PSL v2 for more details.

"""


from typing import List, Dict, Optional, Generator, Any, Union, Literal, Tuple, Callable

import random

import Musicreater


from .constants import (
    CLT_ANSI,
    CLT_MC,
    MIDI_PITCH_GROUP,
    MINECRAFT_COLOUR_MAP,
    MINECRAFT_CODE_ANSI,
    MINECRAFT_NOTE_INSTRUMENT_COLOUR_MAP,
    MINECRAFT_NONGRAY_COLOUR_LIST,
    MINECRAFT_GRAY_BW_COLOUR_LIST,
)
from .subclasses import (
    FloatingNote,
    DirectionPointing,
    DirectionCoordinate,
    CoordinateAxis,
)
from .types import ColourStatementType
from .exceptions import AxisParallelError


def gen_colour_map(
    instrument_list: List[str],
    base_colour_map: Dict[str, str] = MINECRAFT_NOTE_INSTRUMENT_COLOUR_MAP,
    alter_colours: List[str] = MINECRAFT_NONGRAY_COLOUR_LIST,
) -> Dict[str, str]:
    """
    生成 音符对应我的世界格式化代码颜色表

    Parameters
    ----------
    instrument_list: list
        音符列表
    base_colour_map: dict[str, str]
        基础颜色对应表，可由用户传入，默认为金羿定义的表
    alter_colours: list[str]
        替代表，当出现基础颜色对应表中没有的乐器、且该表中所有颜色被耗尽时采用，此时，该替代表中的颜色会被随机分配给上述乐器

    Returns
    -------
    colour_map: dict
        音符对应我的世界格式化代码颜色表
    """

    # 对于在所给乐器表中的乐器，使用颜色表中对应的颜色
    not_done_instruments = instrument_list.copy()
    using_colour_map = base_colour_map.copy()
    unused_colours = []
    for instrument in base_colour_map.keys():
        if instrument not in instrument_list:
            unused_colours.append(using_colour_map.pop(instrument))
        else:
            not_done_instruments.remove(instrument)

    # 对于不在表中的乐器，随机使用除了已经使用的颜色的其他颜色
    if len(not_done_instruments) > 0:
        random.shuffle(unused_colours)
        for i in range(min(len(unused_colours), len(not_done_instruments))):
            using_colour_map[not_done_instruments[i]] = unused_colours[i]
            not_done_instruments.pop(0)

        # 当颜色表被耗尽，则使用另外使用现有颜色生成一个新的表，并随机颜色
        if len(not_done_instruments) > 0:
            colours_list = alter_colours.copy()
            while len(not_done_instruments) > 0:
                # 如果这一行代码真的能被执行，那这首曲子真的是奇葩
                random.shuffle(colours_list)
                for i in range(min(len(colours_list), len(not_done_instruments))):
                    using_colour_map[not_done_instruments[i]] = colours_list[i]
                    not_done_instruments.pop(0)

    # 完美
    return using_colour_map


copy_floating_note_line: Callable[
    [
        List[FloatingNote],
    ],
    List[FloatingNote],
] = lambda line: [i.copy() for i in line]
"""
拷贝一个音符行
"""


def dropping_notes(
    msct_msq: Musicreater.MusicSequence,
    colour_map: Dict[str, str],
    screen_width: int = 100,
    gen_beginning_whitelines: int = 0,
    gen_trailing_whitelines: int = 0,
) -> Generator[Tuple[List[FloatingNote], int], Any, None]:
    """
    生成下落的音符

    Parameters
    ----------
    msct_msq: Musicreater.MusicSequence
        音乐序列
    colour_map: dict[str, str]
        音符对应我的世界格式化代码颜色表
    screen_width: int
        屏幕宽度
    gen_beginning_whitelines: int
        在开始生成一些空白行，为零则不生成
    gen_trailing_whitelines: int
        在最后生成一些空白行，为零则不生成

    Yields
    -------
    tuple[list[FloatingNote], int]
        下落的音符列表，起始音符下标
    """

    # 所有音符按照时间排序
    notes_list: List[Musicreater.MineNote] = sorted(
        [i for j in msct_msq.channels.values() for i in j],
        key=lambda note: note.start_tick,
    )

    now_note_index = 0
    """
    遍历到音符的索引
    """

    screen_line = [FloatingNote.empty() for i in range(128)]
    """
    单音符行
    """

    if gen_beginning_whitelines:
        for i in range(gen_beginning_whitelines):
            yield [FloatingNote.empty() for i in range(128)], (0)

    # 用来计算在当前屏幕宽度下，显示开始音符的列表下标，这个 pitch 对应的就是下标
    last_center_pitch: float = -1
    start_pitch: float = -1

    # 天才般的设计
    for i in range(notes_list[-1].start_tick):

        # 找出所有在当前刻播出的音符，并将其加入当前行
        while (k := notes_list[now_note_index]).start_tick <= i:
            screen_line[k.note_pitch].parallel(
                FloatingNote.from_mine_note(k, colour_map.get(k.sound_name, ""))
            )
            now_note_index += 1

        # 计算屏幕宽度适应，请注意这段代码中千万不要更改 screen_line 的音符值
        if screen_width < 128:

            # 获取有多少个音符在当前行
            line_pitch: List[int] = [1] * 128
            # start_pitch_change = (-1, 128)
            min_pitch = 129
            max_pitch = -1
            for scr_note in screen_line:
                if scr_note.remain >= 0:
                    min_pitch = min(min_pitch, scr_note.pitch)
                    max_pitch = max(max_pitch, scr_note.pitch)
                    line_pitch[scr_note.pitch] += 1

            # 不会出现了
            # # 一个音符也没有就随便返回个空的去
            # if len(line_pitch) == 0:
            #     yield copy_floating_note_line(screen_line), (
            #         0 if start_pitch == -1 else int(start_pitch)
            #     )
            #     continue

            # 计算当前音符音高平均值，最大最小值
            center_pitch = sum([i * j for i, j in enumerate(line_pitch)]) / len(
                line_pitch
            )

            if start_pitch == -1:
                # 第一个有音符的循环，一定要先算，否则后面的 start_pitch 都是错的怎么算
                start_pitch = center_pitch - screen_width / 2
                last_center_pitch = center_pitch
            # elif (max_pitch - min_pitch > screen_width) and (int(last_center_pitch) == int(center_pitch)):
            #     pass
            elif int(last_center_pitch) != int(center_pitch):
                # 如果需要移动（计算出来的中心点发生偏移）
                if max_pitch - min_pitch > screen_width:
                    # 音符所在的音高都已经大于可以显示的长度了，速度多少都无所谓了
                    start_pitch += (last_center_pitch - center_pitch) / 20
                else:
                    # if start_pitch < min_pitch and start_pitch + screen_width > max_pitch:
                    #     # 音符的音高全部在显示范围之中，不做变化
                    #     pass
                    # else:
                    #     # 音符的音高有部分在显示范围之外，需要变化
                    #     start_pitch += int((last_center_pitch - center_pitch) / 5)
                    if (
                        start_pitch > min_pitch
                        or start_pitch + screen_width < max_pitch
                    ):
                        # 与上面同理，非(甲 与 乙) <=> (非甲)与(非乙)，修改代码时请保留上面的过程
                        start_pitch += (last_center_pitch - center_pitch) / 10
                last_center_pitch = center_pitch
            # 别走出去了
            start_pitch = min(max(start_pitch, 0), 128 - screen_width)

            yield copy_floating_note_line(screen_line), round(start_pitch)

        else:
            # 输出的音符是不包含其他预备覆盖的音符的拷贝
            yield copy_floating_note_line(screen_line), 0

        # 对所有当前行的音符进行迭代，让其存在时间少 1 刻，具体实现请看类中函数
        for scr_note in screen_line:
            scr_note.iterate()

    if gen_trailing_whitelines:

        for i in range(gen_trailing_whitelines):
            yield [FloatingNote.empty() for i in range(128)], (
                int(start_pitch) if start_pitch else 0
            )


def mc_2_ansi(mc_coloured_screen: str, enable_the_convertion: bool = True) -> str:
    if enable_the_convertion:
        for to_rep, replacement in MINECRAFT_CODE_ANSI.items():
            mc_coloured_screen = mc_coloured_screen.replace(to_rep, replacement)
    return mc_coloured_screen


def gen_dropping_notes_screen(
    msct_msq: Musicreater.MusicSequence,
    colour_map: Dict[str, str],
    screen_width: int = 100,
    screen_height: int = 20,
    colour_type: ColourStatementType = CLT_ANSI,
) -> Generator[str, Any, None]:

    dump_cache: List[List[FloatingNote]] = []
    stt_index_cache: List[int] = []

    for _lne, _stt in dropping_notes(
        msct_msq, colour_map, screen_width - 10, screen_height + 1, screen_height + 1
    ):
        dump_cache.insert(0, _lne)
        stt_index_cache.insert(0, _stt)

        if len(dump_cache) > screen_height - (2 if colour_type == CLT_ANSI else 1):

            start_index = stt_index_cache.pop(-1)

            yield mc_2_ansi(
                "\n".join(
                    [
                        "".join(
                            [
                                i.__str__()
                                for i in sgl[
                                    start_index : start_index + screen_width - 10
                                ]
                            ]
                        )
                        for sgl in dump_cache
                    ]
                )
                + "\n"
                + "".join(
                    [
                        dump_cache[-1][i].colourify(
                            "#"
                            if "#" in (k := Musicreater.MIDI_PITCH_NAME_TABLE[i])
                            else k
                        )
                        for i in range(start_index, start_index + screen_width - 10)
                    ]
                ),
                colour_type == CLT_ANSI,
            ) + (
                (
                    "\n\033[K"
                    + "".join(
                        [
                            "\033[{}G{}".format(
                                i - start_index + 1, MIDI_PITCH_GROUP.get(i, "")
                            )
                            for i in range(start_index, start_index + screen_width - 10)
                            if i in MIDI_PITCH_GROUP
                        ]
                    )
                )
                if colour_type == CLT_ANSI
                else ""
            )
            dump_cache.pop(-1)


def construct_piano_structure(
    base_point: DirectionCoordinate,
    toward_direction: DirectionPointing = DirectionPointing(CoordinateAxis.Z, 1),
    tune_direction: DirectionPointing = DirectionPointing(CoordinateAxis.X, 1),
    surface_direction: DirectionPointing = DirectionPointing(CoordinateAxis.Y, 1),
    piano_blackkey_length: int = 4,
    piano_whitekey_length: int = 7,
    command_per_delay: int = 2,
    execute_command_header: str = "execute as @p at @s run ",
) -> List[Musicreater.MineCommand]:
    if (
        toward_direction.coordinate_axis.perpendicular(tune_direction.coordinate_axis)
        != surface_direction.coordinate_axis
    ):
        raise AxisParallelError(
            "琴键面对方向 {}，钢琴音高增加方向 {}，琴键表面方向 {} 必须互相垂直。".format(
                toward_direction, tune_direction, surface_direction
            )
        )

    base_point = base_point.copy()

    final_result = [
        # 钢琴底座-底板
        # 甲=======乙
        # |  琴 键  |
        # 丙-------丁
        Musicreater.MineCommand(
            execute_command_header
            + "fill {} {} black_terracotta".format(
                base_point.move(toward_direction, 1)
                .move(tune_direction, 1)
                .to_minecraft_relative(),
                (
                    end_point := (
                        side_point_2 := base_point.move_noupdate(
                            toward_direction, piano_whitekey_length + 2
                        )
                    ).move_noupdate(tune_direction, 106)
                ).to_minecraft_relative(),
            ),
            tick_delay=command_per_delay,
            annotation="钢琴底座",
        ),
        # 丙-甲-乙-丁 护板
        # 甲=======乙
        # ‖  琴 键  ‖
        # 丙-------丁
        Musicreater.MineCommand(
            execute_command_header
            + 'fill {} {} quartz_pillar ["pillar_axis"="{}"]'.format(
                (
                    up_side_point_2 := side_point_2.move_noupdate(surface_direction, 2)
                ).to_minecraft_relative(),
                (
                    up_base_point := base_point.move_noupdate(surface_direction, 2)
                ).to_minecraft_relative(),
                toward_direction.coordinate_axis.value.lower(),
            ),
            tick_delay=command_per_delay,
            annotation="钢琴护板 甲-丙",
        ),
        Musicreater.MineCommand(
            execute_command_header
            + 'fill {} {} quartz_pillar ["pillar_axis"="{}"]'.format(
                up_base_point.to_minecraft_relative(),
                (
                    up_side_point_1 := (
                        side_point_1 := base_point.move_noupdate(tune_direction, 106)
                    ).move_noupdate(surface_direction, 2)
                ).to_minecraft_relative(),
                tune_direction.coordinate_axis.value.lower(),
            ),
            tick_delay=command_per_delay,
            annotation="钢琴护板 甲-乙",
        ),
        Musicreater.MineCommand(
            execute_command_header
            + 'fill {} {} quartz_pillar ["pillar_axis"="{}"]'.format(
                up_side_point_1.to_minecraft_relative(),
                (
                    up_end_point := end_point.move_noupdate(surface_direction, 2)
                ).to_minecraft_relative(),
                toward_direction.coordinate_axis.value.lower(),
            ),
            tick_delay=command_per_delay,
            annotation="钢琴护板 乙-丁",
        ),
        # 丙-丁 护手板
        # 甲=======乙
        # |  琴 键  |
        # 丙-------丁
        Musicreater.MineCommand(
            execute_command_header
            + "fill {} {} waxed_cut_copper_slab".format(
                side_point_2.move_noupdate(
                    surface_direction, 1
                ).to_minecraft_relative(),
                end_point.move_noupdate(surface_direction, 1).to_minecraft_relative(),
            ),
            tick_delay=command_per_delay,
            annotation="钢琴护手板 丙-丁",
        ),
        # 钢琴底座-基柱
        # 甲=======乙
        # |  琴 键  |
        # 丙-------丁
        Musicreater.MineCommand(
            execute_command_header
            + "fill {} {} black_terracotta".format(
                base_point.to_minecraft_relative(),
                up_base_point.to_minecraft_relative(),
            ),
            tick_delay=command_per_delay,
            annotation="钢琴底座-基柱-甲",
        ),
        Musicreater.MineCommand(
            execute_command_header
            + "fill {} {} black_terracotta".format(
                side_point_1.to_minecraft_relative(),
                up_side_point_1.to_minecraft_relative(),
            ),
            tick_delay=command_per_delay,
            annotation="钢琴底座-基柱-乙",
        ),
        Musicreater.MineCommand(
            execute_command_header
            + "fill {} {} black_terracotta".format(
                side_point_2.to_minecraft_relative(),
                up_side_point_2.to_minecraft_relative(),
            ),
            tick_delay=command_per_delay,
            annotation="钢琴底座-基柱-丙",
        ),
        Musicreater.MineCommand(
            execute_command_header
            + "fill {} {} black_terracotta".format(
                end_point.to_minecraft_relative(),
                up_end_point.to_minecraft_relative(),
            ),
            tick_delay=command_per_delay,
            annotation="钢琴底座-基柱-丁",
        ),
        # 白键
        Musicreater.MineCommand(
            execute_command_header
            + "fill {} {} white_concrete".format(
                base_point.move(surface_direction, 1)
                .move(toward_direction, 1)
                .move(tune_direction, 1)
                .to_minecraft_relative(),
                end_point.move(surface_direction, 1)
                .move(toward_direction, -1)
                .move(tune_direction, -1)
                .to_minecraft_relative(),
            ),
            tick_delay=command_per_delay,
            annotation="白键",
        ),
    ]

    base_point.move(surface_direction, 1)

    # 键缝
    for i in range(51):
        base_point.move(tune_direction, 2)
        final_result.append(
            Musicreater.MineCommand(
                execute_command_header
                + "fill {} {} redstone_wire".format(
                    base_point.to_minecraft_relative(),
                    base_point.move_noupdate(
                        toward_direction, piano_whitekey_length
                    ).to_minecraft_relative(),
                ),
                tick_delay=command_per_delay,
                annotation="第 {} 个键缝".format(i),
            )
        )

    up_base_point.move(tune_direction, 2).move(toward_direction, 1)

    # 黑键
    for i in range(25, 124):
        up_base_point.move(tune_direction, 1)
        if i % 14 in (1, 3, 7, 9, 11):
            final_result.append(
                Musicreater.MineCommand(
                    execute_command_header
                    + "fill {} {} black_concrete".format(
                        up_base_point.to_minecraft_relative(),
                        up_base_point.move_noupdate(
                            toward_direction, piano_blackkey_length
                        ).to_minecraft_relative(),
                    ),
                    tick_delay=command_per_delay,
                    annotation="第 {} 列的黑键".format(i),
                )
            )

    return final_result


def mc_colour_2_block(mc_colour: str) -> str:
    """
    将我的世界格式化代码颜色转换为方块名称，方便 fill 或 setblock
    """
    return MINECRAFT_COLOUR_MAP.get(mc_colour, {}).get("BE_BLOCK", "red_wool")


def gen_dropping_into_piano(
    msq: Musicreater.MusicSequence,
    colour_map: Dict[str, str],
    centre_point: DirectionCoordinate,
    drop_direction: DirectionPointing,
    expand_direction: DirectionPointing,
    upinto_direction: DirectionPointing,
    drop_distance: int = 20,
    blackkey_length: int = 4,
    whitekey_length: int = 7,
):

    begin_point = centre_point.copy().move(expand_direction, -54)

    piano_construct_commands = construct_piano_structure(
        base_point=begin_point.move_noupdate(upinto_direction, -2),
        toward_direction=drop_direction,
        tune_direction=expand_direction,
        surface_direction=upinto_direction,
        piano_blackkey_length=blackkey_length,
        piano_whitekey_length=whitekey_length,
        command_per_delay=2,
        execute_command_header="execute as @e[type=armor_stand, name=piano] at @s run ",
    )

    note_generator = dropping_notes(
        msq, colour_map, 129, drop_distance + 1, drop_distance + 1
    )

    # 列表最开始的是要运行到最下面的音符
    dump_cache: List[List[FloatingNote]] = [
        next(note_generator)[0] for i in range(drop_distance - 1)
    ]

    key_point = (
        begin_point.copy()
        .move(expand_direction, 2)
        .move(drop_direction, 2)
        .move(upinto_direction, -1)
    )

    begin_point.move(expand_direction, -23)

    _last_lne = [FloatingNote.empty() for i in range(128)]
    result_note_down: List[List[Musicreater.MineCommand]] = []

    for _lne, _stt in note_generator:
        notes_down_commands: List[Musicreater.MineCommand] = []
        dump_cache.append(_lne)

        line_point = begin_point.copy()

        for line in dump_cache:

            now_point = line_point.copy()

            notes_down_commands.append(
                Musicreater.MineCommand(
                    "execute as @e[type=armor_stand, name=piano] at @s run fill {} {} air".format(
                        now_point.to_minecraft_relative(),
                        now_point.move_noupdate(
                            expand_direction, 156
                        ).to_minecraft_relative(),
                    ),
                    tick_delay=0,
                    annotation="清空当前行音符",
                )
            )
            for i in range(128):

                if i % 12 in (0, 5):
                    now_point.move(expand_direction, 2)
                else:
                    now_point.move(expand_direction, 1)

                if _note := line[i]:
                    notes_down_commands.append(
                        Musicreater.MineCommand(
                            "execute as @e[type=armor_stand, name=piano] at @s run setblock {} {}".format(
                                now_point.to_minecraft_relative(),
                                mc_colour_2_block(_note.colour),
                            ),
                            tick_delay=0,
                            annotation="放置音符{}".format(
                                _note.colourify(str(_note.pitch))
                            ),
                        )
                    )

            line_point.move(drop_direction, -1)
        # if len(dump_cache) >= drop_distance:
        #     for i in range(drop_distance):
        #         pass

        _now_line = dump_cache.pop(0)

        now_point = key_point.copy()

        for i in range(21, 109):
            if i % 12 in (0, 5):
                now_point.move(expand_direction, 2)
            else:
                now_point.move(expand_direction, 1)
            if _now_line[i] and (not _last_lne[i]):
                if i % 12 in (1, 3, 6, 8, 10):
                    notes_down_commands.append(
                        Musicreater.MineCommand(
                            "execute as @e[type=armor_stand, name=piano] at @s run fill {} {} polished_blackstone_slab".format(
                                now_point.move_noupdate(
                                    upinto_direction, 1
                                ).to_minecraft_relative(),
                                now_point.move_noupdate(upinto_direction, 1)
                                .move_noupdate(drop_direction, blackkey_length)
                                .to_minecraft_relative(),
                            ),
                            tick_delay=0,
                            annotation="按下键 {}(黑)".format(i),
                        )
                    )
                else:
                    notes_down_commands.append(
                        Musicreater.MineCommand(
                            "execute as @e[type=armor_stand, name=piano] at @s run fill {} {} smooth_quartz_slab".format(
                                now_point.to_minecraft_relative(),
                                now_point.move_noupdate(
                                    drop_direction, whitekey_length
                                ).to_minecraft_relative(),
                            ),
                            tick_delay=0,
                            annotation="按下键 {}(白)".format(i),
                        )
                    )
            elif (not _now_line[i]) and _last_lne[i]:
                if i % 12 in (1, 3, 6, 8, 10):
                    notes_down_commands.append(
                        Musicreater.MineCommand(
                            "execute as @e[type=armor_stand, name=piano] at @s run fill {} {} black_concrete".format(
                                now_point.move_noupdate(
                                    upinto_direction, 1
                                ).to_minecraft_relative(),
                                now_point.move_noupdate(upinto_direction, 1)
                                .move_noupdate(drop_direction, blackkey_length)
                                .to_minecraft_relative(),
                            ),
                            tick_delay=0,
                            annotation="放回键 {}(黑)".format(i),
                        )
                    )
                else:
                    notes_down_commands.append(
                        Musicreater.MineCommand(
                            "execute as @e[type=armor_stand, name=piano] at @s run fill {} {} white_concrete".format(
                                now_point.to_minecraft_relative(),
                                now_point.move_noupdate(
                                    drop_direction, whitekey_length
                                ).to_minecraft_relative(),
                            ),
                            tick_delay=0,
                            annotation="放回键 {}(白)".format(i),
                        )
                    )
        result_note_down.append(notes_down_commands)
        # notes_down_commands.append(
        #     Musicreater.MineCommand("", tick_delay=1, annotation="延时占位 1 刻")
        # )
        _last_lne = _now_line

    return piano_construct_commands, result_note_down
